iT邦幫忙

2025 iThome 鐵人賽

DAY 23
0
Modern Web

我與型別的 30 天約定:TypeScript 入坑實錄系列 第 23

Day 23|泛型進階:打造可重用且型別安全的工具函式

  • 分享至 

  • xImage
  •  

1) 引言:為什麼要泛型?

在 JavaScript 裡,我們常常會寫一些「通用」的函式,例如:

ts
CopyEdit
function first(arr) {
  return arr[0];
}

但這個函式有兩個問題:

  1. 沒有型別保護 → first([1,2,3]) 回傳的型別 TS 只會推成 any
  2. 無法知道回傳值的型別與輸入的關係 → 例如 string[] 應該回傳 string

泛型(Generics)就是為了解決這種問題而存在的。

它讓我們可以把 型別變數化,讓函式、型別或類別可以針對「任意型別」運作,但同時保留型別推論與檢查能力。


2) 泛型的基礎語法

泛型用尖括號 <T> 宣告,T 就是「型別參數」:

ts
CopyEdit
function first<T>(arr: T[]): T {
  return arr[0];
}

const num = first([1, 2, 3]);     // num: number
const str = first(["a", "b"]);    // str: string

這裡的好處是:

  • 呼叫時,TS 自動推論 T 是什麼型別(不必手動指定)
  • 回傳型別與輸入型別精確對應

3) 手動指定泛型型別

有時候推論不準,我們可以手動給型別參數:

ts
CopyEdit
const mixed = first<number | string>([1, "b", 3]); // T 被強制為 number|string


4) 泛型型別限制(Constraints)

有時候,我們希望泛型不是真的「任何型別」,而是必須符合某些條件。

例如:要取得 .length,就必須保證傳入的東西有 length 屬性。

ts
CopyEdit
function getLength<T extends { length: number }>(value: T) {
  return value.length;
}

getLength([1, 2, 3]);  // OK
getLength("Hello");    // OK
getLength(123);        // ❌ number 沒有 length


5) 多個泛型參數

泛型可以有多個參數:

ts
CopyEdit
function merge<T, U>(a: T, b: U): T & U {
  return { ...a, ...b };
}

const user = merge({ id: 1 }, { name: "Alice" });
// user: { id: number; name: string }

這在合併物件、建立複合資料型別時非常好用。


6) 泛型預設型別

如果呼叫時沒傳型別參數,可以給一個預設:

ts
CopyEdit
function withDefault<T = string>(value: T): T {
  return value;
}

withDefault("hi");  // T = string
withDefault(123);   // T = number


7) 實戰應用 1:泛型 API 呼叫工具

前面 Day 20~22 我們一直在處理 API 型別,這裡我們做一個泛型 API 函式:

ts
CopyEdit
async function fetchJson<T>(url: string, options?: RequestInit): Promise<T> {
  const res = await fetch(url, options);
  if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
  return res.json() as Promise<T>;
}

// 使用
type User = { id: string; name: string; email: string };
const user = await fetchJson<User>("/api/users/1");
console.log(user.name); // 有型別提示!

好處:

  • 型別與資料結構保持一致
  • 任何 API 回傳的資料型別都能自動檢查

8) 實戰應用 2:泛型結合工具型別

我們可以結合 Day 21 的 PickOmit

ts
CopyEdit
async function fetchPartial<T, K extends keyof T>(
  url: string,
  keys: K[]
): Promise<Pick<T, K>> {
  const res = await fetch(url);
  const data = await res.json();
  const result = {} as Pick<T, K>;
  keys.forEach(k => (result[k] = data[k]));
  return result;
}

// 使用
const partialUser = await fetchPartial<User, "id" | "name">("/api/users/1", ["id", "name"]);

這樣可以只取得我們要的欄位。


9) 實戰應用 3:泛型表單驗證器

搭配 zod

ts
CopyEdit
import { z } from "zod";

function validate<T>(schema: z.ZodSchema<T>, data: unknown): T {
  return schema.parse(data);
}

const userSchema = z.object({
  id: z.string(),
  name: z.string(),
});

const validUser = validate(userSchema, { id: "1", name: "Alice" });


10) 常見錯誤與心法

錯誤 1:過度使用泛型

ts
CopyEdit
function foo<T>(value: T) { return value; }
// 如果沒必要,其實可以直接寫 function foo(value: string)

錯誤 2:忘了加泛型限制,導致型別太寬

ts
CopyEdit
function getName<T>(obj: T) {
  return obj.name; // ❌ TS 不知道 T 有 name
}

→ 應該加 <T extends { name: string }>

心法

  • 泛型不是越多越好,要針對可變的地方才加
  • 要先確保型別安全,再考慮抽象化

上一篇
Day 22|型別守衛與類型窄化:讓 TypeScript 幫你聰明收斂型別
下一篇
Day 24|條件型別:讓型別也能寫 if/else
系列文
我與型別的 30 天約定:TypeScript 入坑實錄24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言